﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Collections.Concurrent;
using System.Reflection.Emit;
using PServiceBus.Services.Gateway.Runtime;
using ReflectionBroker;
using PServiceBus.Connectors;

namespace PServiceBus.RemoteProxy {
    public class ESBProxy {

        private static readonly ConcurrentDictionary<Type, Type> CachedType = new ConcurrentDictionary<Type, Type>();
        private static readonly ConcurrentDictionary<string, Type> CachedRequestType = new ConcurrentDictionary<string, Type>();
        private const MethodAttributes ConstructorAttribute = MethodAttributes.Public | MethodAttributes.HideBySig;

        private const BindingFlags PropertyBinding = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public;

        private const MethodAttributes MethodAttribute =
            MethodAttributes.Public
            | MethodAttributes.Virtual
            | MethodAttributes.Final
            | MethodAttributes.HideBySig
            | MethodAttributes.NewSlot
            | MethodAttributes.SpecialName;


        public static T Create<T>() where T : class {
            var proxyType = CreateProxy<T>();
            var proxy = Activator.CreateInstance(proxyType);
            return proxy as T;
        }

        private static Type VoidType = typeof(void);

        private static Type ObjectType = typeof(object);

        private static Type ActionType = typeof(Action<>);

        private static Type WrapperType = typeof(ESBResponseWrapper<>);

        private static Type ESBMessageRequestType = typeof(ESBMessageRequest<,>);

        private static Type ESBMessageResponseType = typeof(ESBMessageResponse);

        private static MethodInfo SelectTopicMethod =
            typeof(Topic).GetMethod("Select", new[] { typeof(string) });

        private static MethodInfo SetParameterMethod =
            typeof(Topic).GetMethod("SetParameter");

        private static MethodInfo PublishMethod =
                typeof(Topic).GetMethod("Publish", Type.EmptyTypes);

        private static MethodInfo FromSecondMethod =
            typeof(TimeSpan).GetMethod("FromSeconds", new[] { typeof(double) });

        private static MethodInfo ESBAuthenticateMethod =
            typeof(ESB).GetMethod("Authenticate",
            BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
            null, new[] { typeof(String), typeof(String) }, null);

        private static MethodInfo ESBConfigWithWCFAddressMethod =
            typeof(ESB).GetMethod("ConfigWithWCFAddress",
            BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
            null, new[] { typeof(String) }, null);

        private static ConstructorInfo ObjectCtr =
            typeof(Object).GetConstructor(
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null,
            Type.EmptyTypes, null);

        private static Type CreateESBMessageRequestType(Type requestType, Type responseType) {
            return CreateType(requestType, responseType, (t1, t2) => ESBMessageRequestType.MakeGenericType(t1, t2), useSelfAsParent: true);
        }

        private static Type CreateESBMessageResponseType(Type responseType) {
           return CreateType(responseType, ESBMessageResponseType, (t1,t2) => ESBMessageResponseType);
        }

        private static Type CreateType(Type sourceType, Type parentType, Func<Type, Type, Type> typeFunc, bool useSelfAsParent = false) {
            var typeName = sourceType.Name;
            var key = typeName + "_" + parentType.Name.Replace(".","").Replace("`", "");
            if (CachedRequestType.ContainsKey(key)) return CachedRequestType[key];
            var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(key + "ESBMessageClass") { Version = new Version(1, 0, 0, 0) },
                AssemblyBuilderAccess.RunAndSave);
            var module = assembly.DefineDynamicModule(typeName + ".dll");
            var type = module.DefineType(typeName, TypeAttributes.Public | TypeAttributes.Serializable, ObjectType);

            type.SetParent(typeFunc(sourceType, parentType));

            if (useSelfAsParent) type.SetParent(typeFunc(type, parentType));

            var props = sourceType.GetPropertiesEx();

            foreach (var prop in props) {
                var customAttributes = prop.GetCustomAttributes(true);

                var getSetProp = type.DefineProperty(prop.Name, PropertyAttributes.HasDefault,
                                                     prop.PropertyType, Type.EmptyTypes);

                foreach (var attribute in customAttributes) {
                    var attributeCtor = attribute.GetType().GetConstructor(Type.EmptyTypes);
                    if (attributeCtor == null) continue;
                    getSetProp.SetCustomAttribute(new CustomAttributeBuilder(attributeCtor, new object[] { }));
                }

                var getSetGetMethod = prop.GetGetMethod();
                var getSetSetMethod = prop.GetSetMethod();

                var setGetField = type.DefineField("_" + prop.Name.ToLower(), prop.PropertyType, FieldAttributes.Private);

                var setMethod = type.DefineMethod(getSetSetMethod.Name, MethodAttribute);
                setMethod.SetReturnType(typeof(void));
                setMethod.SetParameters(prop.PropertyType);
                var setGen = setMethod.GetILGenerator();

                setGen.Emit(OpCodes.Ldarg_0);
                setGen.Emit(OpCodes.Ldarg_1);
                setGen.Emit(OpCodes.Stfld, setGetField);
                setGen.Emit(OpCodes.Ret);

                var getMethod = type.DefineMethod(getSetGetMethod.Name, MethodAttribute);
                getMethod.SetReturnType(prop.PropertyType);
                var getGen = getMethod.GetILGenerator();

                getGen.Emit(OpCodes.Ldarg_0);
                getGen.Emit(OpCodes.Ldfld, setGetField);
                getGen.Emit(OpCodes.Ret);

                getSetProp.SetGetMethod(getMethod);
                getSetProp.SetSetMethod(setMethod);
            }

            var returnType = type.CreateType();
            CachedRequestType[key] = returnType;
            return returnType;
        }

        private static Type CreateProxy<T>() where T : class {
            var classType = typeof(T);
            if (!classType.IsInterface) throw new Exception("Type must be an interface");
            var classAttributes = classType.GetCustomAttributes(typeof(ESBProxyAttribute), true).Cast<ESBProxyAttribute>().ToList();
            if (!classAttributes.Any()) return classType;
            if (CachedType.ContainsKey(classType)) return CachedType[classType];
            var newTypeName = String.Concat(classType.Name.Substring(1), "ESBClassProxy");
            var proxyEndpoint = classAttributes[0].Endpoint;
            var proxyUserName = classAttributes[0].UserName;
            var proxyPassword = classAttributes[0].Password;

            var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(newTypeName) { Version = new Version(1, 0, 0, 0) },
                                                                           AssemblyBuilderAccess.RunAndSave);
            var module = assembly.DefineDynamicModule(newTypeName + ".dll");
            var type = module.DefineType(newTypeName, TypeAttributes.Public | TypeAttributes.Serializable, ObjectType, new[] { classType });

            //Define constructor
            var ctor = type.DefineConstructor(MethodAttributes.Public 
                | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName |
                MethodAttributes.HideBySig, CallingConventions.Standard, Type.EmptyTypes);

            var ctorIL = ctor.GetILGenerator();
            ctorIL.Emit(OpCodes.Ldarg_0);
            ctorIL.Emit(OpCodes.Call, ObjectCtr);
            ctorIL.Emit(OpCodes.Nop);
            ctorIL.Emit(OpCodes.Nop);
            ctorIL.Emit(OpCodes.Ldstr, proxyUserName);
            ctorIL.Emit(OpCodes.Ldstr, proxyPassword);
            ctorIL.Emit(OpCodes.Call, ESBAuthenticateMethod);
            ctorIL.Emit(OpCodes.Nop);
            ctorIL.Emit(OpCodes.Ldstr, proxyEndpoint);
            ctorIL.Emit(OpCodes.Call, ESBConfigWithWCFAddressMethod);
            ctorIL.Emit(OpCodes.Nop);
            ctorIL.Emit(OpCodes.Nop);
            ctorIL.Emit(OpCodes.Ret);

            //Define methods
            var methods = classType.GetMethods();

            foreach (var method in methods) {
                if (method.Name.StartsWith("set_") || method.Name.StartsWith("get_")) continue;

                var isProxy = method.GetCustomAttributes(typeof(ESBProxyIgnoreAttribute), true).Count() == 0;
                var methodName = method.Name;
                var parameters = method.GetParameters();
                var methodReturnType = method.ReturnType;
                var isVoid = methodReturnType == typeof(void);

                var methodBuilder = type.DefineMethod(methodName, MethodAttribute, method.ReturnType,
                    parameters.Select(p => p.ParameterType).ToArray());

                var methodIL = methodBuilder.GetILGenerator();

                if (isProxy) {
                    if (isVoid) {
                        var index = 0;
                        var local = methodIL.DeclareLocal(typeof(Topic));
                        methodIL.Emit(OpCodes.Nop);
                        methodIL.Emit(OpCodes.Ldstr, methodName);
                        methodIL.Emit(OpCodes.Call, SelectTopicMethod);
                        methodIL.Emit(OpCodes.Stloc_0);

                        foreach (var parameter in parameters) {
                            methodIL.Emit(OpCodes.Ldloc_0);
                            methodIL.Emit(OpCodes.Ldstr, parameter.Name);
                            methodIL.Emit(OpCodes.Ldarg, ++index);
                            methodIL.Emit(OpCodes.Callvirt, SetParameterMethod);
                            methodIL.Emit(OpCodes.Pop);
                        }

                        methodIL.Emit(OpCodes.Ldloc_0);
                        methodIL.Emit(OpCodes.Callvirt, PublishMethod);
                        methodIL.Emit(OpCodes.Nop);
                    } else {
                        var requestType = parameters[0].ParameterType;
                        var responseESBMessageType = CreateESBMessageResponseType(methodReturnType);
                        var requestESBMessageType = CreateESBMessageRequestType(requestType, responseESBMessageType);
                        var messageRequestGenericType = ESBMessageRequestType.MakeGenericType(requestESBMessageType, methodReturnType);
                        var wrapperType = WrapperType.MakeGenericType(responseESBMessageType);
                        var wrapperTypeResponseField = wrapperType.GetField("Response");
                        var wrapper = methodIL.DeclareLocal(wrapperType);
                        var requestESBMessage = methodIL.DeclareLocal(requestESBMessageType);
                        var responseESBMessage = methodIL.DeclareLocal(methodReturnType);
                        var returnMessage = methodIL.DeclareLocal(methodReturnType);
                        var label = methodIL.DefineLabel();

                        methodIL.Emit(OpCodes.Nop);
                        methodIL.Emit(OpCodes.Newobj, wrapperType.GetConstructor(Type.EmptyTypes));
                        methodIL.Emit(OpCodes.Stloc_0);
                        methodIL.Emit(OpCodes.Newobj, requestESBMessageType.GetConstructor(Type.EmptyTypes));
                        methodIL.Emit(OpCodes.Stloc_1);

                        var requestProps = requestType.GetPropertiesEx();

                        foreach (var requestProp in requestProps) {
                            var getRequestValueMethod = requestProp.GetGetMethod();
                            var setRequestESBMessageValue = requestESBMessageType.GetPropertyEx(requestProp.Name).GetSetMethod();
                            methodIL.Emit(OpCodes.Ldloc_1);
                            methodIL.Emit(OpCodes.Ldarg_1);
                            methodIL.Emit(OpCodes.Callvirt, getRequestValueMethod);
                            methodIL.Emit(OpCodes.Callvirt, setRequestESBMessageValue);
                            methodIL.Emit(OpCodes.Nop);
                        }
                        

                        methodIL.Emit(OpCodes.Ldloc_1);
                        methodIL.Emit(OpCodes.Ldloc_0);
                        methodIL.Emit(OpCodes.Ldftn, wrapperType.GetMethodEx("Handle"));
                        methodIL.Emit(OpCodes.Newobj, ActionType.MakeGenericType(responseESBMessageType).GetConstructor(new[] { typeof(object), typeof(IntPtr) }));
                        methodIL.Emit(OpCodes.Callvirt, messageRequestGenericType.GetMethodEx("OnReply"));
                        methodIL.Emit(OpCodes.Pop);
                        methodIL.Emit(OpCodes.Ldloc_1);
                        methodIL.Emit(OpCodes.Ldc_I4_1);
                        methodIL.Emit(OpCodes.Callvirt, messageRequestGenericType.GetMethodEx("WaitForResponse"));
                        methodIL.Emit(OpCodes.Pop);
                        methodIL.Emit(OpCodes.Ldloc_1);
                        methodIL.Emit(OpCodes.Ldc_R8, 30.0);
                        methodIL.Emit(OpCodes.Call, FromSecondMethod);
                        methodIL.Emit(OpCodes.Callvirt, messageRequestGenericType.GetMethodEx("TimeOut"));
                        methodIL.Emit(OpCodes.Pop);
                        methodIL.Emit(OpCodes.Ldloc_1);
                        methodIL.Emit(OpCodes.Callvirt, messageRequestGenericType.GetMethodEx("Send"));
                        methodIL.Emit(OpCodes.Nop);
                        
                        //Added
                        methodIL.Emit(OpCodes.Newobj, methodReturnType.GetConstructor(Type.EmptyTypes));
                        methodIL.Emit(OpCodes.Stloc_2);

                        var responseProps = methodReturnType.GetPropertiesEx();
                        foreach (var responseProp in responseProps) {
                            var getResponseValueMethod = responseESBMessageType.GetPropertyEx(responseProp.Name).GetGetMethod();
                            var setResponseESBMessageValue = responseProp.GetSetMethod();
                            methodIL.Emit(OpCodes.Ldloc_2);
                            methodIL.Emit(OpCodes.Ldloc_0);
                            methodIL.Emit(OpCodes.Ldfld, wrapperTypeResponseField);
                            methodIL.Emit(OpCodes.Callvirt, getResponseValueMethod);
                            methodIL.Emit(OpCodes.Callvirt, setResponseESBMessageValue);
                            methodIL.Emit(OpCodes.Nop);
                        }

                        methodIL.Emit(OpCodes.Ldloc_2);
                        methodIL.Emit(OpCodes.Stloc_3);
                        methodIL.Emit(OpCodes.Br_S, label);
                        methodIL.MarkLabel(label);
                        methodIL.Emit(OpCodes.Ldloc_3);
                    }
                } else 
                    methodIL.Emit(OpCodes.Nop);
                
                methodIL.Emit(OpCodes.Ret);
                type.DefineMethodOverride(methodBuilder, method);
            }

            var returnType = type.CreateType();
            CachedType[typeof(T)] = returnType;
            return returnType;
        }
    }
}
